package com.spring.sso;

import com.spring.sso.config.AuthenticationConfig;
import com.spring.sso.config.security.authentication.RemoteAuthenticationFilter;
import com.spring.sso.config.security.authentication.TokenAuthenticationFilter;
import com.spring.sso.config.security.authentication.VerificationAuthenticationFailEntryPoint;
import com.spring.sso.config.security.logout.LogoutSuccessHandler;
import com.spring.sso.config.security.logout.SsoLogoutHandler;
import com.spring.sso.config.security.manage.SsoAuthenticationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author : pangfuzhong
 * @description
 * @date : 2021/9/20 15:04
 *
 *  单点登陆认证:
 *
 *      1、当前web应用未登录
 *
 *      2、其他应用登陆
 */
@EnableWebSecurity
public class ClientWebSecurityConfigureAdapter extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationConfig authenticationConfig;

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 忽略options预请求, spring security将会忽略options类型的浏览器发送的预请求
        web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
        web.ignoring().antMatchers("/templates/**", "/config/**", "/css/**", "/fonts/**", "/img/**", "/js/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 允许跨域访问
        http.cors();
        // 关闭跨站请求伪造
        http.csrf().disable();
        // 关闭session(前后端分离无需使用到session)
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http
            .formLogin()
            // 用户未登录时，访问任何资源都转跳到该路径，即登录页面
            .loginPage("/login")
            // 登录表单form中action的地址，也就是处理认证请求的路径
            .loginProcessingUrl("/login")
            // 表单提交时的key, 即前端按照此格式传参, 如果addFilter中重新new了UsernamePasswordAuthenticationFilter或其实现类,如下两个配置将会失效, 下面采用了重写,此处注释
            //.usernameParameter("username")
            //.passwordParameter("password")

            // 默认登陆成功后跳转到此 ，如果 alwaysUse 为 true 只要进行认证流程而且成功，会一直跳转到此。一般推荐默认值 false, 等同于 successForwardUrl()
            .successForwardUrl("/unifiedLoginSuccess")
            // 只有在UserDetailsService实现类出现的认证异常才会转发到此, 而LoginController中的异常不会, 一般前后分离用到它。 可定义一个 Controller （控制器）来处理返回值,但是要注意 RequestMethod
            .failureForwardUrl("/unifiedLoginFailure")

            // note: 如果配置了successHandler和failureHandler将会覆盖spring内置的为successForwardUrl和failureForwardUrl路径配置的处理器
            //.successHandler(new SsoLoginAuthenticationSuccessHandler())
            //.failureHandler(new SsoLoginAuthenticationFailureHandler())
            .permitAll();
        http.authorizeRequests()
            //添加/product/** 下的所有请求只能由user角色才能访问
            //.antMatchers("/product/**").hasAnyRole("USER")
            //添加/admin/** 下的所有请求只能由admin角色才能访问
            //.antMatchers("/admin/**").hasRole("ADMIN")
            // 允许浏览器options预请求
            .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() //不需要通过登录验证就可以被访问的资源路径
            .antMatchers(HttpMethod.POST, "/logout").permitAll()//不需要通过登录验证就可以被访问的资源路径
            .anyRequest().authenticated(); /// 没有定义的请求，所有的角色都可以访问（tmp也可以）


        // 退出操作
        http.logout().addLogoutHandler(new SsoLogoutHandler())// 退出成功处理, 将会让logoutSuccessUrl失效
            .logoutSuccessHandler(new LogoutSuccessHandler())// 退出成功后的跳转页面
            .logoutSuccessUrl("/login");

        // token 认证过滤器
        http.addFilter(new TokenAuthenticationFilter(authenticationManager(), this.authenticationConfig, new VerificationAuthenticationFailEntryPoint()));
        // 执行认证
        RemoteAuthenticationFilter remoteAuthenticationFilter = new RemoteAuthenticationFilter();
        remoteAuthenticationFilter.setAuthenticationManager(new SsoAuthenticationManager(this.authenticationConfig));
        remoteAuthenticationFilter.setUsernameParameter("userName");
        remoteAuthenticationFilter.setPasswordParameter("password");
        remoteAuthenticationFilter.setAuthenticationSuccessHandler(null);
        remoteAuthenticationFilter.setAuthenticationFailureHandler(null);
        http.addFilter(remoteAuthenticationFilter);

        //开启记住我功能,session和cookie实现，默认两周时间
        //http.rememberMe().rememberMeParameter("rememberme");
    }

    /**
     *    1、使用Spring Security时，一定要在Spring Security认证前解决跨域问题。因为在跨域的场景下，浏览器在发送真实请求前会先发送一个预请求（简单请求不会发送）。
     *
     *  预请求的请求方法是OPTIONS，没有携带数据，目的是测试从浏览器到服务器通不通。如果请求成功，就证明从浏览器到服务器通，那么浏览器就会发送真实的请求。
     *
     *    2、如果服务器没有在Spring Security之前处理掉预请求，那么在用户进行登录操作时，Spring Security就会对预请求进行拦截。因为预请求不携带数据（token、session等），
     *
     *  因此Spring Security便无法为其进行认证与授权，而是直接拒绝服务。这也就导致用户无法登录
     *
     * */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 携带服务器端验证后允许的跨域请求域名, 即 Access-Control-Allow-Origin 参数, 即本服务允许该参数指定的域名进行跨域访问
        configuration.addAllowedOrigin("*");
        // 服务端告知浏览器当前端配置了withCredentials属性为true时，在跨域访问时带上认证信息(cookie) 即 Access-Control-Allow-Credentials
        configuration.setAllowCredentials(true);
        // 服务端告知浏览器可以在实际发送跨域请求时，支持的请求类型, 即 Access-Control-Allow-Headers
        configuration.addAllowedHeader(CorsConfiguration.ALL);
        // 服务端告知浏览器可以在实际发送跨域请求时，跨域请求支持的方法, 即 Access-Control-Allow-Methods
        configuration.addAllowedMethod(CorsConfiguration.ALL);
        // 当不同源的的网站所在浏览器发起的请求都按照此跨域配置进行
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}
